﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM.CSS
{
    #region Using Statements

    using System;
    using KLF.Lib.Browser.DOM.Style;
    using System.Collections.Generic;
    using System.Text.RegularExpressions;

    #endregion

    /// <summary>
    /// Class that is responsible for parsing CSS text.
    /// </summary>
    public class CSSParser
    {
        /// <summary>
        /// A regex for parsing out css text.
        /// </summary>
        static Regex ruleMatcher = new Regex(
            @"(?<cssComment>(?:\/\*).*?(?:\*\/))|" +
            @"(?<cssMedia>\@media)|" +
            @"(?<cssImport>\@import\s+url\s*[\(""]+(?<cssImportUrl>[^""\)]+)[""\)]+)|" +
            @"(?<cssText>[a-zA-Z0-9\-\=\<\>\[\]\*\|\.\s\:\(\)#~\+]+)|" +
            @"(?<cssColon>[:]{1})|" +
            @"(?<cssStyleOpen>[\{])|" +
            @"(?<cssStyleClose>[\{])|" +
            @"(?<cssPropertySeparator>[\;])|" +
            @"(?<cssSelectorSeparator>[\,])",
            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

        CSSParserContext Context { get; set; }

        /// <summary>
        /// Construct a new CSS parser.
        /// </summary>
        public CSSParser(string cssText)
        {
            Context = new CSSParserContext(cssText);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public StyleSheet Build()
        {
            CSSStyleSheet sheet = new CSSStyleSheet();
            List<CSSParserToken> tokens = Parse();
            List<string> selectorList = new List<string>();
            bool isInStyleList = false;

            foreach (CSSParserToken token in tokens)
            {
                switch (token.Type)
                {
                    case CSSParserTokenType.CssAttributeSeparator:
                        break;
                    case CSSParserTokenType.CssComment:
                        break;
                    case CSSParserTokenType.CssPropertyName:
                    case CSSParserTokenType.CssPropertyValue:
                    case CSSParserTokenType.CssSelectorSeparator:
                        break;
                    case CSSParserTokenType.CssStyleOpen:
                        break;
                    case CSSParserTokenType.CssStyleClose:

                        break;
                    case CSSParserTokenType.CssText:
                        if (!isInStyleList)
                        {
                            selectorList.Add(token.RawText);
                        }
                        break;
                }
            }

            return sheet;
        }

        /// <summary>
        /// 
        /// </summary>
        internal List<CSSParserToken> Parse()
        {
            while (!Context.EndOfString)
            {
                Match match = ruleMatcher.Match(Context.CSSText, Context.Index);

                if (match.Success)
                {
                    CSSParserToken token = null;

                    if (match.Groups["cssComment"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssComment,
                            match.Groups["cssComment"].Value);
                    }
                    else if (match.Groups["cssStyleOpen"].Success)
                    {
                        Context.InStyle = true;
                        token = new CSSParserToken(
                            CSSParserTokenType.CssStyleOpen,
                            match.Groups["cssStyleOpen"].Value);
                    }
                    else if (match.Groups["cssStyleClose"].Success)
                    {
                        Context.InStyle = false;
                        token = new CSSParserToken(
                            CSSParserTokenType.CssStyleClose,
                            match.Groups["cssStyleClose"].Value);
                    }
                    else if (match.Groups["cssPropertySeparator"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssAttributeSeparator,
                            match.Groups["cssPropertySeparator"].Value);
                    }
                    else if (match.Groups["cssSelectorSeparator"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssSelectorSeparator,
                            match.Groups["cssSelectorSeparator"].Value);
                    }
                    else if (match.Groups["cssText"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssText,
                            match.Groups["cssText"].Value);
                    }
                    else if (match.Groups["cssImport"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssImport,
                            match.Groups["cssImport"].Value);
                    }
                    else if (match.Groups["cssMedia"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssMedia,
                            match.Groups["cssMedia"].Value);
                    }
                    Context.Index = match.Index + token.RawText.Length;
                    token.RawText = token.RawText.Trim();

                    if (token.Type == CSSParserTokenType.CssImport)
                        token.RawText = match.Groups["cssImportUrl"].Value;

                    if (token != null && token.RawText.Length > 0)
                    {

                        int index = token.RawText.IndexOf(':');
                        if (index > -1 && Context.InStyle)
                        {
                            string propertyName = token.RawText.Substring(0, index).Trim();
                            string propertyValue = token.RawText.Substring(index + 1).Trim();

                            Context.Tokens.Add(new CSSParserToken(CSSParserTokenType.CssPropertyName, propertyName));
                            Context.Tokens.Add(new CSSParserToken(CSSParserTokenType.CssPropertyValue, propertyValue));
                        }
                        else
                        {
                            Context.Tokens.Add(token);
                        }
                    }
                }
                else
                {
                    var str = Context.CSSText.Substring(Context.Index);
                    Context.Tokens.Add(new CSSParserToken(CSSParserTokenType.CssText, str));
                    return Context.Tokens;
                }
            }
            return Context.Tokens;
        }
    }

    /// <summary>
    /// What type of token is it.
    /// </summary>
    internal enum CSSParserTokenType
    {
        CssComment,
        CssMedia,
        CssImport,
        CssStyleOpen,
        CssStyleClose,
        CssPropertyName,
        CssPropertyValue,
        CssSelectorSeparator,
        CssAttributeSeparator,
        CssText
    }

    /// <summary>
    /// A token read from the css stream.
    /// </summary>
    internal class CSSParserToken
    {
        static Regex wsCleanup = new Regex(@"[\s]+");

        internal CSSParserToken(CSSParserTokenType _type, String _raw)
        {
            RawText = _raw;
            Type = _type;
        }

        internal string RawText { get; set; }

        internal string CleanText 
        {
            get
            {
                return wsCleanup.Replace(RawText.Trim(), " ");
            }
        }

        internal CSSParserTokenType Type { get; set; }

        public override string ToString()
        {
            return CleanText;
        }
    }

    internal class CSSParserContext
    {
        internal CSSParserContext(string css)
        {
            CSSText = css;
            Tokens = new List<CSSParserToken>();
        }

        internal string CSSText { get; private set; }
        internal int Index { get; set; }
        internal List<CSSParserToken> Tokens { get; set; }
        internal bool EndOfString { get { return Index >= CSSText.Length; } }
        internal bool InStyle { get; set; }
    }
}
